# -*- coding: utf-8 -*-

from datetime import datetime
import logging
from collections import OrderedDict
import traceback, sys

from flask import current_app
from werkzeug.security import generate_password_hash, check_password_hash
from sqlalchemy.dialects import mysql
from sqlalchemy.exc import SQLAlchemyError

from mpdev import db
from mpdev.gzone_loader import UserMixin

logger = logging.getLogger(__name__)


class Permission:
    ADD_MOMENT = 0x01
    UPDATE_PROFILE = 0x02
    UPLOAD_FILE = 0x04
    DELETE_FILE = 0x08
    UPLOAD_IMAGE = 0x10
    DELETE_IMAGE = 0x20
    ADD_ANNOUNCEMENT = 0x20000000
    UPDATE_ANNOUCEMENT = 0x40000000


class Role(db.Model):
    __tablename__ = 'role'

    id = db.Column(mysql.TINYINT, primary_key=True)
    name = db.Column(db.String(16), nullable=False)
    permission = db.Column(db.Integer, nullable=False)

    roles = OrderedDict(
        user=Permission.ADD_MOMENT | Permission.UPDATE_PROFILE\
            | Permission.UPLOAD_FILE | Permission.UPLOAD_IMAGE,
        admin=Permission.ADD_MOMENT | Permission.UPDATE_PROFILE\
                | Permission.UPLOAD_FILE | Permission.DELETE_FILE\
                | Permission.UPLOAD_IMAGE | Permission.DELETE_IMAGE\
                | Permission.ADD_ANNOUNCEMENT | Permission.UPDATE_ANNOUCEMENT,
        owner=Permission.ADD_MOMENT | Permission.UPDATE_PROFILE\
                | Permission.UPLOAD_FILE | Permission.DELETE_FILE\
                | Permission.UPLOAD_IMAGE | Permission.DELETE_IMAGE\
                | Permission.ADD_ANNOUNCEMENT | Permission.UPDATE_ANNOUCEMENT,
    )

    @classmethod
    def load(cls):
        for role_name, p in cls.roles.items():
            role = cls.query.filter(cls.name == role_name).first()
            if role is None:
                role = cls(name=role_name, permission=p)
                db.session.add(role)
                db.session.commit()

    def __str__(self):
        return '<{}: {}>'.format(self.name, hex(self.permission))


class RoomAccount(db.Model):
    __tablename = 'room_account'

    id = db.Column(mysql.BIGINT, primary_key=True)
    join_time = db.Column(mysql.TIMESTAMP, nullable=False, default=datetime.now)
    status = db.Column(mysql.TINYINT(4), nullable=False, default=1)
    last_visit = db.Column(mysql.TIMESTAMP, nullable=False, default=datetime.now)
    account_openid = db.Column(db.String(128))

    room_id = db.Column(db.String(32), db.ForeignKey('room.id'))
    account_wxid = db.Column(db.String(32), db.ForeignKey('account.wxid'))

    def visit(self):
        self.last_visit = datetime.now()
        db.session.add(self)
        db.session.commit()

    @classmethod
    def refresh_room_user(cls, users, room_id):
        # logger.debug('Room user info from redis: Room: %s. Users: %s', room_id, users)
        wxids_m = set(r[0] for r in db.session.query(cls.account_wxid)
                      .filter_by(room_id=room_id).all())
        wxids_r = set(users.keys())
        not_existed_wxids = wxids_r - wxids_m
        no_longer_existed_wxids = wxids_m - wxids_r
        add_room_accounts = [cls(room_id=room_id, account_wxid=wxid)
                             for wxid in not_existed_wxids]
        db.session.bulk_save_objects(add_room_accounts)
        logger.info(
            'Add new user to room_account table: %s',
            [r_a.account_wxid for r_a in add_room_accounts]
        )
        if no_longer_existed_wxids:
            cls.query.filter_by(room_id=room_id).filter(cls.account_wxid.in_(no_longer_existed_wxids))\
                .update({'status': 0}, synchronize_session='fetch')
            logger.info('Disable user to room_account table: %s', no_longer_existed_wxids)
        cls.query.filter_by(room_id=room_id).filter(cls.account_wxid.in_(wxids_r))\
            .update({'status': 1}, synchronize_session='fetch')
        logger.info('Enable user to room_account table: %s', wxids_r)
        db.session.commit()

    @property
    def message_count(self):
        # todo: add more precision criteria
        return Message.query.filter_by(
            room_id=self.room_id,
            publisher_wxid=self.account_wxid,
            type_=1
        ).count()

    @property
    def signin_count(self):
        return current_app.redis_interface.get_signin_count(self.room_id, self.account_wxid)

    @property
    def successive_signin_count(self):
        return current_app.redis_interface.get_successive_signin_count(
            self.room_id,
            self.account_wxid
        )


class Room(db.Model):
    __tablename__ = 'room'

    id = db.Column(db.String(32), primary_key=True)
    name = db.Column(db.String(32))
    nickname = db.Column(db.String(128))
    sticky_announce_id = db.Column(mysql.BIGINT)
    status = db.Column(mysql.TINYINT, nullable=False, default=1)
    create_time = db.Column(mysql.TIMESTAMP, nullable=False, default=datetime.now)

    accounts = db.relationship(
        'RoomAccount',
        backref=db.backref('room', lazy='joined'),
        lazy='dynamic',
        cascade='all, delete-orphan'
    )

    photos = db.relationship(
        'Asset',
        lazy='dynamic',
        primaryjoin="and_(Asset.room_id==Room.id, "
                    "Asset.type_==1)"
    )

    files = db.relationship(
        'Asset',
        lazy='dynamic',
        primaryjoin="and_(Asset.room_id==Room.id, "
                    "Asset.type_==2)"
    )

    group_images = db.relationship(
        'Message',
        lazy='dynamic',
        primaryjoin="and_(Message.room_id==Room.id, "
                    "Message.type_==3)"
    )

    def is_joined(self, account_wxid):
        return self.accounts.filter_by(account_wxid=account_wxid).first() is not None

    @property
    def sticky_announcement(self):
        return Announcement.query.filter_by(id=self.sticky_announce_id).first()

    def set_sticky_announcement(self, sticky_ann_id):
        self.announcements.filter_by(id=sticky_ann_id).one()
        self.sticky_announce_id = sticky_ann_id
        db.session.commit()

    # @property
    # def photos(self):
    #     return Asset.query.filter_by(status=1, room_id=self.id, type_=1)
    #
    # @property
    # def files(self):
    #     return Asset.query.filter_by(status=1, room_id=self.id, type_=2)

    @property
    def enabled(self):
        return self.status == 1

    @property
    def disabled(self):
        return not self.enabled

    def enable(self):
        self.status = 1
        db.session.add(self)
        db.session.commit()

    def disable(self):
        self.status = 0
        db.session.add(self)
        db.session.commit()

    @classmethod
    def get_instance(cls, room_id):
        ins = cls(id=room_id)
        db.session.add(ins)
        db.session.commit()
        return ins


class Account(UserMixin, db.Model):
    __tablename__ = 'account'

    id = db.Column(db.Integer, primary_key=True)
    wxid = db.Column(db.String(32), nullable=False, unique=True)
    openid = db.Column(db.String(128), unique=True)
    name = db.Column(db.String(64))
    password_hash = db.Column('password', db.String(128))
    nickname = db.Column(db.String(64))
    headimgurl = db.Column(db.String(256))
    sex = db.Column(mysql.TINYINT, default=0)
    country = db.Column(db.String(32))
    province = db.Column(db.String(32))
    city = db.Column(db.String(32))
    language = db.Column(db.String(8))
    email = db.Column(db.String(64))
    tel = db.Column(db.String(32))
    company = db.Column(db.String(32))
    title = db.Column(db.String(32))
    desc = db.Column(db.String(128))
    # subscribe_time = db.Column(mysql.TIMESTAMP, nullable=False)
    bound_time = db.Column(
        mysql.TIMESTAMP,
        nullable=False,
        default=datetime.now,
    )
    update_time = db.Column(
        mysql.TIMESTAMP,
        nullable=False,
        default=datetime.now,
        onupdate=datetime.now,
    )
    refresh_token = db.Column(db.String(512))
    status = db.Column(mysql.TINYINT, nullable=False, default=1)

    # 默认角色为普通用户
    role_id = db.Column(
        mysql.TINYINT,
        db.ForeignKey('role.id'),
        nullable=False,
        default=1
    )

    role = db.relationship(
        'Role',
        backref=db.backref('accounts', lazy='dynamic')
    )
    rooms = db.relationship(
        'RoomAccount',
        backref=db.backref('account', lazy='joined'),
        lazy='dynamic',
        cascade='all, delete-orphan'
    )
    photos = db.relationship(
        'Asset',
        lazy='dynamic',
        primaryjoin='and_(Asset.owner_openid==Account.openid, '
                    'Asset.type_==1)'
    )
    files = db.relationship(
        'Asset',
        lazy='dynamic',
        primaryjoin='and_(Asset.owner_openid==Account.openid, '
                    'Asset.type_==2)'
    )
    banner = db.relationship(
        'Asset',
        lazy='joined',
        primaryjoin='and_(Asset.owner_openid==Account.openid, '
                    'Asset.type_==3)',
        uselist=False
    )

    @property
    def banner_asset(self):
        if self.banner is None:
            self.banner = Asset(
                type_=Asset.ACCOUNT_BANNER,
                owner_openid=self.openid,
            )
        return self.banner

    @property
    def password(self):
        return self.password_hash

    @password.setter
    def password(self, value):
        # 增加密码的hash算法
        self.password_hash = Account.password_hasher(value)

    @staticmethod
    def password_hasher(password):
        # 增加密码的hash算法
        return generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def is_active(self):
        return self.enabled

    def can(self, permission):
        pass

    def is_in(self, room):
        return self.rooms.filter_by(room_id=room.id).first() is not None

    def join(self, room):
        room_account = self.rooms.filter_by(room_id=room.id).first()
        if room_account is not None:
            room_account.last_visit = datetime.now()
            room_account.account_openid = self.openid
            db.session.add(room_account)
            db.session.commit()
        return room_account

    def expired(self, expiration):
        now = datetime.now()
        elapsed = now - self.bound_time
        if elapsed.total_seconds() >= expiration:
            logger.info(
                u'User info in DB EXPIRED: Wxid: %s. Openid: %s. '
                u'NOW: %s, LAST BOUND: %s. Expiration: %s. Elapsed: %s',
                self.wxid,
                self.openid,
                now,
                self.bound_time,
                expiration,
                elapsed.total_seconds()
            )
            return True
        return False

    @classmethod
    def get_instance(cls, room, **kwargs):
        # load_user()中已经确保存在openid
        openid = kwargs.pop('openid')
        nickname = kwargs.get('nickname')
        # todo: 增加nickname的filter
        account = cls.query.filter(cls.openid == openid).first()
        if account is None:
            accounts = cls.query.join(RoomAccount)\
                .filter(cls.nickname == nickname, RoomAccount.room_id == room.id).all()
            count = len(accounts)
            if count != 1:
                return None
            account = accounts[0]
            account.openid = openid

        if kwargs:
            for key, val in kwargs.items():
                setattr(account, key, val)
            db.session.add(account)
            try:
                db.session.commit()
                logger.info(
                    'Update account info according oauth ret: %s. Account: %s, %s.',
                    kwargs,
                    account.wxid,
                    account.openid
                )
            except SQLAlchemyError:
                db.session.rollback()

                exc_type, exc_value, exc_tb = sys.exc_info()
                logger.error(
                    u'Update account %s info FAILED. '
                    u'Error: %s. TB: %s. wxid: %s. Openid: %s.',
                    account.id,
                    exc_value,
                    traceback.format_tb(exc_tb),
                    account.wxid,
                    account.openid,
                )
                account = None

        return account

    @classmethod
    def validate_user(cls):
        pass

    @classmethod
    def refresh_room_user(cls, users):
        # todo: 需不需要更新已存在用户的头像?
        # logger.debug('Room user info from redis: %s', users)
        wxids_r = set(users.keys())
        not_existed_wxids = wxids_r - set(
            r[0] for r in
            db.session.query(cls.wxid).filter(cls.wxid.in_(wxids_r)).all()
        )
        account_objs = [cls(wxid=wxid, nickname=users[wxid]['nickname'],
                        headimgurl=users[wxid].get('headimgurl'))
                        for wxid in not_existed_wxids]
        db.session.bulk_save_objects(account_objs)
        logger.info(
            'Add new accounts to account table: %s',
            [(acc.wxid, acc.nickname, acc.headimgurl) for acc in account_objs]
        )

        # 不更新account信息
        # 头像，昵称等信息由redis初次载入信息和OAuth返回信息决定
        # existed = wxids_r - not_existed_wxids
        # for wxid in existed:
        #     account = cls.query.filter_by(wxid=wxid).first()
        #     todo: not safe here
        #     account.nickname = users[wxid].get('nickname')
        #     account.headimgurl = users[wxid].get('headimgurl')
        db.session.commit()


class Admin(db.Model):
    # dummy table for testing
    # In production env, integrated with user table
    __tablename__ = 'admin'

    id = db.Column(mysql.BIGINT, primary_key=True)
    openid = db.Column(db.String(128), nullable=False)
    wxid = db.Column(db.String(32), nullable=False)
    password_hash = db.Column('password', db.String(128), nullable=False)

    @property
    def password(self):
        return self.password_hash

    @password.setter
    def password(self, password):
        self.password_hash = self.password_hasher(password)

    @staticmethod
    def password_hasher(password):
        # 增加密码的hash算法
        return generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password, password)


class Announcement(db.Model):
    __tablename__ = 'announcement'
    id = db.Column(mysql.BIGINT, primary_key=True)
    title = db.Column(db.String(128), nullable=False)
    content = db.Column(db.TEXT, nullable=False)
    img_path = db.Column(db.String(256))
    create_time = db.Column(db.TIMESTAMP, nullable=False, default=datetime.now)
    update_time = db.Column(db.TIMESTAMP, nullable=False, default=datetime.now)
    status = db.Column(mysql.TINYINT(4), nullable=False, default=1)
    room_id = db.Column(db.String(32), db.ForeignKey('room.id'))
    publisher_openid = db.Column(db.String(128), db.ForeignKey('account.openid'))

    room = db.relationship(
        'Room',
        backref=db.backref('announcements', lazy='dynamic')
    )
    publisher = db.relationship(
        'Account',
        backref=db.backref('announcements', lazy='dynamic')
    )


class Message(db.Model):
    # readonly model
    __tablename__ = 'message'

    id = db.Column(mysql.BIGINT, primary_key=True)
    type_ = db.Column('type', mysql.SMALLINT, nullable=False)
    status = db.Column(mysql.TINYINT, nullable=False, default=1)
    create_time = db.Column(mysql.TIMESTAMP, nullable=False)
    room_id = db.Column('room', db.String(32), db.ForeignKey('room.id'))
    publisher_wxid = db.Column('talker', db.String(32))
    content = db.Column(db.String(256))
    filepath = db.Column(db.String(256))

    room = db.relationship(
        'Room',
        backref=db.backref('messages', lazy='dynamic')
    )


class Moment(db.Model):
    __tablename__ = 'moment'

    id = db.Column(mysql.BIGINT, primary_key=True)
    content = db.Column(db.String(256), nullable=False)
    status = db.Column(mysql.TINYINT, nullable=False,  default=1)
    create_time = db.Column(db.TIMESTAMP, nullable=False, default=datetime.now)
    room_id = db.Column(db.String(32), db.ForeignKey('room.id'))
    publisher_openid = db.Column(db.String(128), db.ForeignKey('account.openid'))

    room = db.relationship(
        'Room',
        backref=db.backref('moments', lazy='dynamic')
    )
    publisher = db.relationship(
        'Account',
        backref=db.backref('moments', lazy='dynamic')
    )
    images = db.relationship(
        'Asset',
        lazy='joined',
        foreign_keys='Asset.moment_id'
    )

    def disable(self):
        self.status = 0


class Asset(db.Model):
    __tablename__ = 'asset'

    MOMENT_IMG = 0
    ALBUM_PHOTO = 1
    GROUP_FILE = 2
    ACCOUNT_BANNER = 3

    id = db.Column(mysql.BIGINT, primary_key=True)
    # 0: moment pic
    # 1: album pic
    # 2: group file
    # 3: account banner
    type_ = db.Column('type', mysql.TINYINT, nullable=False)
    filename = db.Column(db.String(128), nullable=False)
    original_filename = db.Column(db.String(256))
    filesize = db.Column(db.Integer)
    status = db.Column(mysql.TINYINT, nullable=False, default=1)
    create_time = db.Column(mysql.TIMESTAMP, nullable=False, default=datetime.now)
    sticky = db.Column(mysql.BIT, nullable=False, default=0)
    owner_openid = db.Column(db.String(128), db.ForeignKey('account.openid'), nullable=False)
    room_id = db.Column(db.String(32), db.ForeignKey('room.id'), nullable=True)
    moment_id = db.Column(mysql.BIGINT, db.ForeignKey('moment.id'), nullable=True)

    owner = db.relationship('Account')

    room = db.relationship('Room')

    moment = db.relationship(
        'Moment',
        backref=db.backref('photos', lazy='dynamic')
    )

    def toggle_sticky(self):
        self.sticky = self.sticky ^ 1
        db.session.add(self)
        db.session.commit()

    def disable(self, preserved=True):
        if preserved:
            self.status = 0
        else:
            db.session.delete(self)
